/*
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla Communicator client code, released
 * March 31, 1998.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1998
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Daniel Veditz <dveditz@netscape.com>
 *   Samir Gehani <sgehani@netscape.com>
 *   Mitch Stoltz <mstoltz@netscape.com>
 *   Jeroen Dobbelaere <jeroen.dobbelaere@acunia.com>
 *   Jeff Walden <jwalden+code@mit.edu>
 *
 *   Lan Qiang <jameslan@gmail.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK *****
 */

#include "nsZipEntryIndex.h"
#include "prprf.h"
#include "prio.h"
#include "zipformat.h"
#include "nsWildCard.h"
#include <stdlib.h>
#include <string.h>

// for PR_FileDesc2NativeHandle
#ifdef XP_MAC
#include "pprio.h"
#else
#include "private/pprio.h"
#endif

// include files for ftruncate (or equivalent)
#if defined(XP_UNIX) || defined(XP_BEOS)
#include <unistd.h>
#elif defined(XP_MAC)
#include <Files.h>
#elif defined(XP_WIN)
#include <windows.h>
#elif defined(XP_OS2)
#define INCL_DOSERRORS
#include <os2.h>
#else
// XXX add necessary include file for ftruncate (or equivalent)
#endif

#ifdef XP_UNIX
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <limits.h>
    #include <unistd.h>
#elif defined(XP_WIN) || defined(XP_OS2)
    #include <io.h>
#elif defined(XP_BEOS)
    #include <unistd.h>
#endif

#ifndef XP_UNIX /* we need some constants defined in limits.h and unistd.h */
#  ifndef S_IFMT
#    define S_IFMT 0170000
#  endif
#  ifndef S_IFLNK
#    define S_IFLNK  0120000
#  endif
#  ifndef PATH_MAX
#    define PATH_MAX 1024
#  endif
#endif  /* XP_UNIX */

static PRUint32 HashName(const char* aName);
static PRUint16 ExtractMode(PRUint32 ll);
static PRUint32 ExtAttrFromMode(nsZipEntryItem *item);
static nsresult strdup(const char *src, char **dst);
PRUint32 PR_Tell(PRFileDesc* aFd);
nsresult PR_Truncate(PRFileDesc* aFd, PRUint32 newEOF);
#if defined(XP_UNIX) || defined(XP_BEOS)
static PRBool IsSymlink(PRUint32 ll);
#endif

////////////////////////////////////////////////////////////////////////////////
// nsZipEntryIndex implementation

#define BR_BUF_SIZE 1024 /* backward read buffer size */

nsZipEntryIndex::nsZipEntryIndex(PRFileDesc* aFd)
	: mFd(aFd),
	  mFirstEntry(nsnull),
	  mLastEntry(nsnull),
	  mEntryCount(0)
{
	memset(mBuckets, 0, sizeof(mBuckets));
}

nsZipEntryIndex::~nsZipEntryIndex()
{
	for (nsZipEntryItem *i = GetBegin(); i;) {
		nsZipEntryItem *temp = i;
		i = i->next;
		free(temp);
	}
}

nsresult nsZipEntryIndex::ReadEntryIndex(PRUint32 *offsetCentralDir)
{
	nsresult rv;

	rv = FindCentralDir(offsetCentralDir);
	NS_ENSURE_SUCCESS(rv, rv);

	if (PR_Seek(mFd, *offsetCentralDir, PR_SEEK_SET) <= 0)
		return NS_ERROR_FILE_CORRUPTED;

	nsZipEntryItem* anItem;
	mEntryCount = 0;
	while (NS_SUCCEEDED(ReadItem(&anItem))) {
		rv = InsertIntoHashTable(anItem);
		NS_ENSURE_SUCCESS(rv, rv);
	}

	return NS_OK;
}

nsresult nsZipEntryIndex::WriteEntryIndex(PRUint32 offset)
{
	// write central directory
	if (PR_Seek(mFd, offset, PR_SEEK_SET) < 0) {
		return NS_ERROR_FILE_CORRUPTED;
	}
	for (nsZipEntryItem *item = GetBegin(); item; item = item->next) {
		if (!item->isSynthetic) {
			nsresult rv = WriteItem(item);
			NS_ENSURE_SUCCESS(rv, rv);
		}
	}

	// write central directory end
	PRUint32 currPos = PR_Tell(mFd);
	ZipCentralEnd zend;

	zend.signature             = host_to_le32(ENDSIG);
	zend.disk_nr               = 0;
	zend.start_central_dir     = 0;
	zend.total_entries_disk    = host_to_le16(mEntryCount);
	zend.total_entries_archive = host_to_le16(mEntryCount);
	zend.central_dir_size      = host_to_le32(currPos - offset);
	zend.offset_central_dir    = host_to_le32(offset);
	zend.commentfield_len      = 0;

	if (PR_Write(mFd, &zend, sizeof(zend)) != sizeof(zend)) {
		return NS_ERROR_FILE_DISK_FULL;
	}

	currPos = PR_Tell(mFd);
	nsresult rv = PR_Truncate(mFd, currPos);
	NS_ENSURE_SUCCESS(rv, rv);

	return NS_OK;
}

nsresult nsZipEntryIndex::GetItem(const char* anEntryName, nsZipEntryItem **anItem)
{
	NS_ENSURE_ARG_POINTER(anItem);

	nsZipEntryItem *item = mBuckets[HashName(anEntryName)];
	while (item) {
		if (!strcmp(anEntryName, item->name)) {
			*anItem = item;
			return NS_OK;
		}
		item = item->hashNext;
	}

	*anItem = nsnull;
	return NS_ERROR_FILE_TARGET_DOES_NOT_EXIST;
}

nsresult nsZipEntryIndex::InsertIntoHashTable(nsZipEntryItem *item)
{
	++mEntryCount;
	// add item to file table
	PRUint32 hash = HashName(item->name);
	// check if the synthetic directory exists
	if (item->isDirectory) {
		for (nsZipEntryItem* zi = mBuckets[hash]; zi != nsnull;
				zi = zi->hashNext)
		{
			if (0 == strcmp(item->name, zi->name)) {
				// a synthetic item already exists,
				// so we need only make the synthetic
				// one unsynthetic
				zi->isSynthetic = PR_FALSE;
				return NS_OK;
			}
		}
	}

	// insert item
	item->hashNext = mBuckets[hash];
	mBuckets[hash] = item;
	if (mLastEntry) {
		mLastEntry->next = item;
		item->prev = mLastEntry;
		mLastEntry = item;
	} else {
		mFirstEntry = mLastEntry = item;
	}

	/* add entries for directories in the current item's path
	 * go from end to beginning, because then we can stop trying
	 * to create diritems if we find that the diritem we want to
	 * create already exists
	 * start just before the last char so as to not add the item
	 * twice if it's a directory
	 */
	PRUint16 namelen = strlen(item->name);
	for (char* p = item->name + namelen - 2; p >= item->name; p--) {
		if ('/' != *p)
			continue;

		PRUint32 dirnamelen = p + 1 - item->name;

		/* See whether we need to create any more implicit directories,
		 * because if we don't we can avoid a lot of work.
		 * We can even avoid (de)allocating space for a bogus dirname
		 * with a little trickery -- save the char at
		 * item->name[dirnamelen], set it to 0, compare the strings,
		 * and restore the saved char when done
		 */
		char savedChar = item->name[dirnamelen];
		item->name[dirnamelen] = 0;

		// Is the directory in the file table?
		PRUint32 hash = HashName(item->name);
		PRBool done = PR_FALSE;
		for (nsZipEntryItem* zi = mBuckets[hash]; zi != NULL; zi = zi->hashNext) {
			if (0 == strcmp(item->name, zi->name)) {
			  // we've already added this dir and all its parents
			  done = PR_TRUE;
			  break;
			}
		}

		// restore the char immediately
		item->name[dirnamelen] = savedChar;

		// if the directory was found, break out of the directory
		// creation loop now that we know all implicit directories
		// are there -- otherwise, start creating the zip item
		if (done)
			break;

		nsZipEntryItem* diritem = (nsZipEntryItem*)
			malloc(sizeof(nsZipEntryItem)+namelen);
		if (!diritem)
			return NS_ERROR_OUT_OF_MEMORY;

		memcpy(diritem->name, item->name, dirnamelen);
		diritem->name[dirnamelen] = 0;

		diritem->isDirectory = PR_TRUE;
		diritem->isSynthetic = PR_TRUE;
		diritem->compression = STORED;
		diritem->size = diritem->realSize = 0;
		diritem->itemCRC32 = 0;
		diritem->mode = 0755;
		diritem->next = nsnull;

		// Set an obviously wrong last-modified date/time, because
		// finding something more accurate like the most recent
		// last-modified date/time of the dir's contents is a lot
		// of effort.  The date/time corresponds to 1980-01-01 00:00.
		diritem->time = 0;
		diritem->date = 1 + (1 << 5) + (0 << 9);

		// add diritem to the file table
		diritem->hashNext = mBuckets[hash];
		mBuckets[hash] = diritem;
		mLastEntry->next = diritem;
		diritem->prev = mLastEntry;
		mLastEntry = diritem;
	} /* end processing of dirs in item's name */
	return NS_OK;
}

nsresult nsZipEntryIndex::RemoveFromHashTable(const char* anEntryName)
{
	PRUint32 hash = HashName(anEntryName);
	nsZipEntryItem *item = mBuckets[hash];

	if (!item) 
		return NS_ERROR_FILE_TARGET_DOES_NOT_EXIST;

	if (!strcmp(anEntryName, item->name)) {
		// remove first entry in this bucket
		mBuckets[hash] = item->hashNext;
		RemoveItem(item);
		free(item);
		return NS_OK;
	}

	// remvoe a non-head entry in the link
	for (; item->hashNext; item = item->hashNext) {
		if (strcmp(anEntryName, item->hashNext->name))
			continue;

		nsZipEntryItem *temp = item->hashNext;
		item->hashNext = temp->hashNext;
		RemoveItem(temp);
		free(temp);
		return NS_OK;
	}

	return NS_ERROR_FILE_TARGET_DOES_NOT_EXIST;
}

void nsZipEntryIndex::RemoveItem(nsZipEntryItem *item)
{
	--mEntryCount;
	if (item->prev) {
		item->prev->next = item->next;
	} else {
		mFirstEntry = item->next;
	}
	if (item->next) {
		item->next->prev = item->prev;
	} else {
		mLastEntry = item->prev;
	}
}

nsresult nsZipEntryIndex::FindCentralDir(PRUint32* offset)
{
	NS_ENSURE_ARG_POINTER(offset);

	PRUint8  buf[4*BR_BUF_SIZE];
	PRUint32 pos;
	pos = PR_Seek(mFd, 0, PR_SEEK_END);
	if (pos <= 0)
		return NS_ERROR_FILE_CORRUPTED;

	for (;;) {
		// read backwards in 1K-sized chunks (or total file)
		PRInt32  bufsize = pos > BR_BUF_SIZE ? BR_BUF_SIZE : pos;
		pos -= bufsize;

		if (PR_Seek(mFd, pos, PR_SEEK_SET) <= 0)
			return NS_ERROR_FILE_CORRUPTED;

		if (PR_Read(mFd, buf, bufsize) != bufsize)
			return NS_ERROR_FILE_CORRUPTED;

		//-- scan for ENDSIG
		PRUint8 *ptr = buf + bufsize;
		for (ptr -= sizeof(ZipCentralEnd); ptr >= buf; --ptr) {
			ZipCentralEnd &end = *(ZipCentralEnd*)ptr;
			if (le32_to_host(end.signature) == ENDSIG) { 
				*offset = le32_to_host(end.offset_central_dir);
				return NS_OK;
			}
		}

		if (pos <= 0)
			// We're at the beginning of the file, and still no sign
			// of the end signature.  File must be corrupted!
			return NS_ERROR_FILE_CORRUPTED;

		// backward read must overlap ZipEnd length
		pos += sizeof(ZipCentralEnd);

	}
	return NS_ERROR_FILE_CORRUPTED;
}

nsresult nsZipEntryIndex::ReadItem(nsZipEntryItem** anItem)
{
	ZipCentralHdr fileHeader;

	PRInt32  byteCount = PR_Read(mFd, &fileHeader, sizeof(fileHeader));
	if (byteCount != sizeof(fileHeader)) {
		return NS_ERROR_FILE_CORRUPTED;
	}

	if (le32_to_host(fileHeader.signature) != CENTRALSIG) {
		return NS_ERROR_FILE_CORRUPTED;
	}

	PRUint16 namelen = le16_to_host(fileHeader.filename_len);
	PRUint16 extralen = le16_to_host(fileHeader.extrafield_len);
	PRUint16 commentlen = le16_to_host(fileHeader.commentfield_len);

	nsZipEntryItem *item = (nsZipEntryItem*)malloc(sizeof(nsZipEntryItem) + namelen);
	if (!item)
		return NS_ERROR_OUT_OF_MEMORY;

	item->next          = nsnull;
	item->prev          = nsnull;
	item->offset        = le32_to_host(fileHeader.localhdr_offset);
	item->dataOffset    = 0;
	item->size          = le32_to_host(fileHeader.size);
	item->realSize      = le32_to_host(fileHeader.orglen);
	item->itemCRC32     = le32_to_host(fileHeader.crc32);
	item->time          = le16_to_host(fileHeader.time);
	item->date          = le16_to_host(fileHeader.date);
	item->isSynthetic   = PR_FALSE;
	item->hasDataOffset = PR_FALSE;
	item->compression   = (PRUint8)le16_to_host(fileHeader.method);
#if defined(XP_UNIX) || defined(XP_BEOS)
	// Check if item is a symlink
	item->isSymlink = IsSymlink(fileHeader.external_attributes);
#endif

	item->mode = ExtractMode(fileHeader.external_attributes);

	// get the item name
	byteCount = PR_Read(mFd, &item->name, namelen);
	if (byteCount != namelen) {
		free(item);
		return NS_ERROR_FILE_CORRUPTED;
	}
	item->name[namelen] = 0;

	// an item whose name ends with '/' is a directory
	item->isDirectory = ('/' == item->name[namelen - 1]);

	// move file pointer to next central file header
	if (PR_Seek(mFd, extralen + commentlen, PR_SEEK_CUR) <= 0) {
		return NS_ERROR_FILE_CORRUPTED;
	}

	*anItem = item;
	return NS_OK;
}

nsresult nsZipEntryIndex::WriteLocalHeader(const nsZipEntryItem *anItem)
{
	ZipLocalHdr header;

	header.signature      = host_to_le32(LOCALSIG);
	header.version        = host_to_le16(20);
	header.bitflag        = 0;
	header.method         = host_to_le16(anItem->compression);
	header.time           = host_to_le16(anItem->time);
	header.date           = host_to_le16(anItem->date);
	header.CRC32          = host_to_le32(anItem->itemCRC32);
	header.size           = host_to_le32(anItem->size);
	header.orglen         = host_to_le32(anItem->realSize);
	header.filename_len   = host_to_le16(strlen(anItem->name));
	header.extrafield_len = 0;

	if (PR_Write(mFd, &header, sizeof(header)) != sizeof(header)) {
		return NS_ERROR_FILE_DISK_FULL;
	}
	if (PR_Write(mFd, anItem->name, header.filename_len) != header.filename_len) {
		return NS_ERROR_FILE_DISK_FULL;
	}

	return NS_OK;
}

nsresult nsZipEntryIndex::WriteItem(nsZipEntryItem *item)
{
	ZipCentralHdr header;

	PRUint16 namelen = strlen(item->name);
	header.signature           = host_to_le32(CENTRALSIG);
	header.version_made_by     = host_to_le16(0x0413);
	header.version             = host_to_le16(20);
	header.bitflag             = 0;
	header.method              = host_to_le16(item->compression);
	header.time                = host_to_le16(item->time);
	header.date                = host_to_le16(item->date);
	header.crc32               = host_to_le32(item->itemCRC32);
	header.size                = host_to_le32(item->size);
	header.orglen              = host_to_le32(item->realSize);
	header.filename_len        = host_to_le16(namelen);
	header.extrafield_len      = 0;
	header.commentfield_len    = 0;
	header.diskstart_number    = 0;
	header.internal_attributes = 0;
	header.external_attributes = ExtAttrFromMode(item);
	header.localhdr_offset     = host_to_le32(item->offset);

	if (PR_Write(mFd, &header, sizeof(header)) != sizeof(header)) {
		return NS_ERROR_FILE_DISK_FULL;
	}

	if (PR_Write(mFd, item->name, namelen) != namelen) {
		return NS_ERROR_FILE_DISK_FULL;
	}

	return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
// nsZipEntryFind member functions

nsZipEntryFind::nsZipEntryFind()
	: mItem(nsnull),
	  mPattern(nsnull),
	  mRegExp(PR_FALSE)
{
}

nsZipEntryFind::~nsZipEntryFind()
{
	delete []mPattern;
}

nsresult nsZipEntryFind::Init(nsZipEntryIndex* anIndex,
		const char* aPattern)
{
	NS_ENSURE_ARG_POINTER(anIndex);
	NS_ENSURE_ARG_POINTER(aPattern);

	nsresult rv;
	rv = strdup(aPattern, &mPattern);
	NS_ENSURE_SUCCESS(rv, rv);

	switch (NS_WildCardValid((char*)aPattern))
	{
	case INVALID_SXP:
		return NS_ERROR_ILLEGAL_VALUE;
	case NON_SXP:
		mRegExp = PR_FALSE;
		mItem = anIndex->mBuckets[HashName(aPattern)];
		break;
	case VALID_SXP:
		mRegExp = PR_TRUE;
		mItem = anIndex->mFirstEntry;
		break;
	default:
		// undocumented return value from RegExpValid!
		return NS_ERROR_ILLEGAL_VALUE;
	}

	return NS_OK;
}

nsresult nsZipEntryFind::FindNext(const char ** aResult)
{
	NS_ENSURE_ARG_POINTER(aResult);

	*aResult = 0;

	while (mItem) {
		nsZipEntryItem *item = mItem;
		if (mRegExp) {
			mItem = mItem->next;
		} else {
			mItem = mItem->hashNext;
		}

		if (!mPattern // always match if no pattern specified
			|| !mRegExp && strcmp(item->name, mPattern) == 0
			|| NS_WildCardMatch(item->name, mPattern, PR_FALSE) == MATCH)
		{
			// matched
			*aResult = item->name;
			return NS_OK;
		}
	}

	return NS_ERROR_FILE_TARGET_DOES_NOT_EXIST;
}

////////////////////////////////////////////////////////////////////////////////
// helper functions

/** 
 * HashName 
 *
 * @return a hash key for the entry name 
 */
static PRUint32 HashName(const char* aName)
{
	NS_ASSERTION(aName != 0, "No entry name");

	PRUint32 val = 0;
	for (PRUint8* c = (PRUint8*)aName; *c != 0; c++) {
		val = val*37 + *c;
	}

	return (val % ZIP_BUCKETCOUNT);
}

/**
 * ExtractMode
 *
 * Extracts bits 17-24 from a 32-bit unsigned long
 * representation of the external attributes field.
 * Subsequently it tacks on the implicit user-read
 * bit.
 */
static PRUint16 ExtractMode(PRUint32 ll)
{
	return (le32_to_host(ll) >> 16) & 0xFF | 0x0100;
}

static PRUint32 ExtAttrFromMode(nsZipEntryItem *item)
{
	PRUint32 mode = (item->mode & 0x1FF) << 16;
#if defined(XP_UNIX) || defined(XP_BEOS)
	if (item->isSymlink) {
		mode |= (S_IFLNK << 16);
	}
#endif
	return host_to_le32(mode);
}

#if defined(XP_UNIX) || defined(XP_BEOS)
/**
 *  Return true if the attributes are for a symbolic link
 *
 */
static PRBool IsSymlink(PRUint32 ll)
{
	return (((le32_to_host(ll) >> 16) & S_IFMT) == S_IFLNK);
}
#endif

static nsresult strdup(const char *src, char **dst)
{
	long len = strlen(src);
	char* buf = new char[len+1];
	if (!buf) {
		return NS_ERROR_OUT_OF_MEMORY;
	}

	memcpy((void*)buf, (void*)src, len+1);
	*dst = buf;
	return NS_OK;
}

PRUint32 PR_Tell(PRFileDesc* aFd)
{
	return PR_Seek(aFd, 0, PR_SEEK_CUR);
}

nsresult PR_Truncate(PRFileDesc* fd, PRUint32 newEOF)
{
	// use modified SetEOF from nsFileStreams::SetEOF()

#if defined(XP_UNIX) || defined(XP_BEOS)
	if (ftruncate(PR_FileDesc2NativeHandle(fd), newEOF) != 0) {
		NS_ERROR("ftruncate failed");
		return NS_ERROR_FAILURE;
	}

#elif defined(XP_MAC)
	if (::SetEOF(PR_FileDesc2NativeHandle(fd), newEOF) != 0) {
		NS_ERROR("SetEOF failed");
		return NS_ERROR_FAILURE;
	}

#elif defined(XP_WIN)
	PRInt32 cnt = PR_Seek(fd, newEOF, PR_SEEK_SET);
	if (cnt == -1)  return NS_ERROR_FAILURE;
	if (!SetEndOfFile((HANDLE) PR_FileDesc2NativeHandle(fd))) {
		NS_ERROR("SetEndOfFile failed");
		return NS_ERROR_FAILURE;
	}

#elif defined(XP_OS2)
	if (DosSetFileSize((HFILE) PR_FileDesc2NativeHandle(fd), newEOF) != NO_ERROR) {
		NS_ERROR("DosSetFileSize failed");
		return NS_ERROR_FAILURE;
	}
#else
	// add implementations for other platforms here
#endif
	return NS_OK;
}
